package cn.clouds234.poi;


import org.apache.poi.ss.usermodel.*;
import org.apache.poi.util.IOUtils;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;

/**
 * Excel相关处理
 * 1. 导入导出菜单时，字典值和枚举值都直接使用编码。不再转换为标签。
 * 2. 枚举值和字典值可以在注解中加入可选值，导入时可以进行校验。
 * 3. 日期类型目前只支持Date和LocalDateTime类型。格式为“yyyy-MM-dd HH:mm:SS”。
 *
 * @author gientech
 */
public class ExcelUtil<T> {

    /**
     * Excel sheet最大行数，默认65536
     */
    public static final int sheetSize = 65536;


    /**
     * 工作薄对象
     */
    private Workbook workbook;

    /**
     * 实体对象
     */
    private Class<T> clazz;
    /**
     * 类字段和在Excel中列序号关系。
     */
    private Map<Integer, Field> fieldsAnnotatedWithExcel;

    public ExcelUtil(Class<T> clazz) {
        this.clazz = clazz;
        this.fieldsAnnotatedWithExcel = getFieldsAnnotatedWithExcel();
    }


    /**
     * excel表转换成list
     * 获取文件流，sheet名称，标题行号。
     * 从文件流获取工作簿。
     * 从工作簿获取sheet表。
     * 判断有数据。
     * 从行对象类获取列号和类字段对应map。
     * 遍历所有数据行。
     * 遍历行内每一列。
     * 取列值添加到类对象。
     * 添加对象到list中。
     * 结束，返回list。
     *
     * @param is 文件输入流
     * @param sheetName sheet名称
     * @param titleNum 标题所在行数。
     * @return 转换后集合
     */
    public List<T> excelToList(InputStream is, String sheetName, int titleNum) throws Exception {
        // 获取工作簿
        this.workbook = WorkbookFactory.create(is);
        // 如果指定sheet名,则取指定sheet中的内容 否则默认指向第1个sheet
        Sheet sheet = sheetName != null && !sheetName.trim().isEmpty() ? workbook.getSheet(sheetName) : workbook.getSheetAt(0);
        if (sheet == null) {
            throw new IOException("文件sheet不存在");
        }
        // 获取最后一个非空行的行下标，比如总行数为n，则返回的为n-1
        int lastRowNum = sheet.getLastRowNum();
        // 判断是否有数据。最后非空行号大于标题行号
        if (lastRowNum > titleNum - 1) {
            // 准备接收数据的list
            List<T> list = new ArrayList<>(100);
            DataFormatter formatter = new DataFormatter();
            // 遍历数据行
            for (int i = titleNum; i <= lastRowNum; i++) {
                // 从表头下一行开始取数据.
                Row row = sheet.getRow(i);
                // 判断当前行是否是空行
                if (isRowEmpty(row)) {
                    continue;
                }
                T entity = null;
                // 遍历注解列
                for (Map.Entry<Integer, Field> numFiled : fieldsAnnotatedWithExcel.entrySet()) {
                    // 获取单元格的值。列序号是从0开始的。所以要减1。
                    String val = formatter.formatCellValue(row.getCell(numFiled.getKey() - 1));
                    // 如果不存在对象则新建.
                    entity = entity == null ? clazz.getDeclaredConstructor().newInstance() : entity;
                    // 从map中得到对应列的属性.
                    Field field = numFiled.getValue();
                    // 根据属性类型和注解中对值的规范格式化单元格值
                    Object value = formValExcelToJava(val, field);
                    // 将单元格值添加到对象中。
                    if (null != val) {
                        field.setAccessible(true);
                        field.set(entity, value);
                    }
                }
                // 将对象添加到list中。
                list.add(entity);
            }
            return list;
        } else {
            return new ArrayList<>();
        }
    }

    /**
     * 根据类字段类型和注解中对值的规定格式化单元格值。
     * 检查值是否符合注解规范。
     *
     * @param val
     * @param field
     * @DATATIME 2025/8/14 21:45
     * @AUTHOR CLOUDS234
     */
    private Object formValExcelToJava(String val, Field field) {
        // 获取属性的类型
        Class<?> fieldType = field.getType();
        // 根据属性类型格式化值
        if (String.class == fieldType) {
            Excel annotation = field.getAnnotation(Excel.class);
            String kind = annotation.kind();
            if (!kind.isEmpty()) {
                String[] kinds = kind.split(",");
                if (Arrays.asList(kinds).contains(val)) {
                    return val;
                } else {
                    throw new RuntimeException(field.getName() + "的值不在规定范围内。");
                }
            }
            return val;
        } else if ((Integer.TYPE == fieldType || Integer.class == fieldType)) {
            return Integer.parseInt(val);
        } else if ((Long.TYPE == fieldType || Long.class == fieldType)) {
            return Long.parseLong(val);
        } else if (Double.TYPE == fieldType || Double.class == fieldType) {
            return Double.parseDouble(val);
        } else if (Float.TYPE == fieldType || Float.class == fieldType) {
            return Float.parseFloat(val);
        } else if (BigDecimal.class == fieldType) {
            return BigDecimal.valueOf(Double.parseDouble(val));
        } else if (Date.class == fieldType) {
            Excel annotation = field.getAnnotation(Excel.class);
            String dateFormat = annotation.dateFormat();
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormat);
            LocalDateTime localDateTime = LocalDateTime.parse(val, formatter);
            return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
        } else if (Boolean.TYPE == fieldType || Boolean.class == fieldType) {
            return Boolean.parseBoolean(val);
        } else if (LocalDateTime.class == fieldType) {
            Excel annotation = field.getAnnotation(Excel.class);
            String dateFormat = annotation.dateFormat();
            DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(dateFormat);
            return LocalDateTime.parse(val, dateTimeFormatter);
        } else if (LocalDate.class == fieldType) {
            Excel annotation = field.getAnnotation(Excel.class);
            String dateFormat = annotation.dateFormat();
            DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(dateFormat);
            return LocalDate.parse(val, dateTimeFormatter);
        } else {
            return val;
        }
    }

    /**
     * 获取类中所有带有 @Excel 注解的字段，以 columnNum 作为 key，字段对象作为 value
     */
    private Map<Integer, Field> getFieldsAnnotatedWithExcel() {
        Map<Integer, Field> resultMap = new HashMap<>();

        // 获取所有声明的字段（包括私有字段）
        Field[] fields = clazz.getDeclaredFields();

        for (Field field : fields) {
            // 检查字段是否有 @Excel 注解
            if (field.isAnnotationPresent(Excel.class)) {
                // 获取注解实例
                Excel excelAnnotation = field.getAnnotation(Excel.class);
                // 将 columNum 作为 key，字段对象作为 value 放入 Map
                resultMap.put(excelAnnotation.columNum(), field);
            }
        }
        return resultMap;
    }
    //==========导出====================

    /**
     * 从list数据导出成Excel文件流。
     * 获取文件输出流，数据列表，表头行号。
     * 创建工作簿，
     * 创建sheet。
     *   添加表头行。
     *     添加列名称。
     *   添加数据行。
     *     添加列数据。
     * 输出流。
     *
     * @param out
     * @param data 为null或空时，只导出带表头的文件。
     * @param sheetName 为空时默认sheet1。
     * @param titleNum 行号大于0;默认是1;
     * @author wyf
     * @date 2025/8/15 15:08
     */
    public void exportExcel(FileOutputStream out, List<T> data, String sheetName, int titleNum) {
        try {
            this.workbook = new SXSSFWorkbook(2000);
            writeSheet(data, sheetName, titleNum);
            workbook.write(out);
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            IOUtils.closeQuietly(workbook);
            IOUtils.closeQuietly(out);
        }
    }

    /**
     * 创建写入数据到Sheet
     */
    private void writeSheet(List<T> dataList, String sheetName, int titleNum) throws IllegalAccessException {
        if (sheetName == null || sheetName.trim().isEmpty()) {
            sheetName = "sheet1";
        }
        Sheet sheet = this.workbook.createSheet(sheetName);
        // 产生标题行
        // 行下标从0开始。所以行号减1;
        titleNum--;
        Row row = sheet.createRow(titleNum);
        // 写入各个字段的列头名称
        for (Map.Entry<Integer, Field> entry :fieldsAnnotatedWithExcel.entrySet()) {
            // 列下标从0开始。所以列序号需要减1。
            this.createCell(entry.getValue().getAnnotation(Excel.class).name(), row, entry.getKey() - 1);
        }
        // 如果没有数据。则直接返回只有标题行的sheet。
        if (dataList == null || dataList.isEmpty()) {
            return;
        }
        // 从标题行下一行开始写入数据行。
        int rowNum = titleNum + 1;
        // 添加数据行
        for (T vo : dataList) {
            // 添加数据行
            row = sheet.createRow(rowNum);
            // 添加列数据
            addCell(row, vo);
            // 下一行
            rowNum++;
        }

    }

    /**
     * 在数据行添加单元格数据。
     * 根据类字段注解，按序号像行中添加数据单元格。
     * @param row
     * @param vo
     * @author wyf
     * @date 2025/8/15 16:21
     */
    private void addCell(Row row, T vo) throws IllegalAccessException {
        for (Map.Entry<Integer, Field> entry : fieldsAnnotatedWithExcel.entrySet()) {
            // 列序号。序号从0开始的。所以减1。
            int columNum = entry.getKey() - 1;
            Cell cell = row.createCell(columNum);
            // 从实体获取字段值
            Field field = entry.getValue();
            field.setAccessible(true);
            Object o = field.get(vo);
            // 单元格添加格式化值
            setFormValJavaToExcel(o, field, cell);
        }
    }

    /**
     * 格式化字段值。
     * Excel中只使用数字和字符串类型。
     * 除了数字类型，其他都用字符串格式。
     * @param val
     * @param field
     * @return Object
     * @author wyf
     * @date 2025/8/15 16:36
     */
    private void setFormValJavaToExcel(Object val, Field field, Cell cell) {
        // 获取属性的类型
        Class<?> fieldType = field.getType();
        // 根据属性类型格式化值
        if (String.class == fieldType) {
            cell.setCellValue(String.valueOf(val));
        } else if ((Integer.TYPE == fieldType || Integer.class == fieldType)) {
            cell.setCellValue((Integer)val);
        } else if ((Long.TYPE == fieldType || Long.class == fieldType)) {
            cell.setCellValue((Long)val);
        } else if (Double.TYPE == fieldType || Double.class == fieldType) {
            cell.setCellValue((Double)val);
        } else if (Float.TYPE == fieldType || Float.class == fieldType) {
            cell.setCellValue((Float)val);
        } else if (BigDecimal.class == fieldType) {
            cell.setCellValue((Double)val);
        } else if (Date.class == fieldType) {
            Excel annotation = field.getAnnotation(Excel.class);
            String dateFormat = annotation.dateFormat();
            SimpleDateFormat sdf = new SimpleDateFormat(dateFormat);
            cell.setCellValue(sdf.format(val));
        } else if (Boolean.TYPE == fieldType || Boolean.class == fieldType) {
            cell.setCellValue(String.valueOf(val));
        } else if (LocalDateTime.class == fieldType) {
            Excel annotation = field.getAnnotation(Excel.class);
            String dateFormat = annotation.dateFormat();
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormat);
            cell.setCellValue(((LocalDateTime)val).format(formatter));
        } else if (LocalDate.class == fieldType) {
            Excel annotation = field.getAnnotation(Excel.class);
            String dateFormat = annotation.dateFormat();
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormat);
            cell.setCellValue(((LocalDate)val).format(formatter));
        } else {
            cell.setCellValue(String.valueOf(val));
        }
    }


    /**
     * 创建表格样式
     *
     * @param wb 工作薄对象
     * @return 样式列表
     */
    private Map<String, CellStyle> createStyles(Workbook wb) {
        // 写入各条记录,每条记录对应excel表中的一行
        Map<String, CellStyle> styles = new HashMap<String, CellStyle>();
        CellStyle style = wb.createCellStyle();
        style.setAlignment(HorizontalAlignment.CENTER);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        Font titleFont = wb.createFont();
        titleFont.setFontName("Arial");
        titleFont.setFontHeightInPoints((short) 16);
        titleFont.setBold(true);
        style.setFont(titleFont);
        styles.put("title", style);

        style = wb.createCellStyle();
        style.setAlignment(HorizontalAlignment.CENTER);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        style.setBorderRight(BorderStyle.THIN);
        style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
        style.setBorderLeft(BorderStyle.THIN);
        style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
        style.setBorderTop(BorderStyle.THIN);
        style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
        style.setBorderBottom(BorderStyle.THIN);
        style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
        Font dataFont = wb.createFont();
        dataFont.setFontName("Arial");
        dataFont.setFontHeightInPoints((short) 10);
        style.setFont(dataFont);
        styles.put("data", style);

        style = wb.createCellStyle();
        style.cloneStyleFrom(styles.get("data"));
        style.setAlignment(HorizontalAlignment.CENTER);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        style.setFillForegroundColor(IndexedColors.GREY_50_PERCENT.getIndex());
        style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
        Font headerFont = wb.createFont();
        headerFont.setFontName("Arial");
        headerFont.setFontHeightInPoints((short) 10);
        headerFont.setBold(true);
        headerFont.setColor(IndexedColors.WHITE.getIndex());
        style.setFont(headerFont);
        styles.put("header", style);

        style = wb.createCellStyle();
        style.setAlignment(HorizontalAlignment.CENTER);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        Font totalFont = wb.createFont();
        totalFont.setFontName("Arial");
        totalFont.setFontHeightInPoints((short) 10);
        style.setFont(totalFont);
        styles.put("total", style);

        style = wb.createCellStyle();
        style.cloneStyleFrom(styles.get("data"));
        style.setAlignment(HorizontalAlignment.LEFT);
        styles.put("data1", style);

        style = wb.createCellStyle();
        style.cloneStyleFrom(styles.get("data"));
        style.setAlignment(HorizontalAlignment.CENTER);
        styles.put("data2", style);

        style = wb.createCellStyle();
        style.cloneStyleFrom(styles.get("data"));
        style.setAlignment(HorizontalAlignment.RIGHT);
        styles.put("data3", style);

        return styles;
    }

    /**
     * 创建单元格
     */
    private Cell createCell(String value, Row row, int column) {
        // 创建列
        Cell cell = row.createCell(column);
        // 写入列信息
        cell.setCellValue(value);
//        cell.setCellStyle(styles.get("header"));
        return cell;
    }

    /**
     * 判断是否是空行
     *
     * @param row 判断的行
     * @return
     */
    private boolean isRowEmpty(Row row) {
        if (row == null) {
            return true;
        }
        for (int i = row.getFirstCellNum(); i < row.getLastCellNum(); i++) {
            Cell cell = row.getCell(i);
            if (cell != null && cell.getCellType() != CellType.BLANK) {
                return false;
            }
        }
        return true;
    }

}
